Sample codes > Sendcloud shipping options (live quotes)
Sendcloud shipping options (live quotes)
This example shows how to integrate Sendcloud live shipping quotes into JsRates using Sendcloud’s Shipping options API.
It demonstrates how to:
- Authenticate with Sendcloud using Basic Auth (API key + secret)
- Build a fetch-shipping-options request using origin/destination + shipment weight (and optional dimensions)
- Fetch available shipping options + quotes
- Convert Sendcloud options into Shopify shipping rates for checkout
Prerequisites
1) Required Sendcloud credentials (JsRates Secrets)
Create the following secrets in Settings → Secrets:
SENDCLOUD_API_KEYSENDCLOUD_API_SECRET
Sendcloud’s Shipping options API supports HTTP Basic Authentication.
Set theAuthorizationheader toBasic base64(API_KEY:API_SECRET).
2) Carrier configuration (Sendcloud account)
To receive shipping options/quotes, you must have:
- a carrier enabled in your Sendcloud account, or
- a connected direct carrier contract
(Availability depends on your Sendcloud configuration.)
Sendcloud API Reference
Shipping options (quotes) are fetched using:
- POST
https://panel.sendcloud.sc/api/v3/fetch-shipping-options
The request supports fields like:
from_country_code,to_country_code(ISO 3166-1 alpha-2)from_postal_code,to_postal_code(recommended for accurate pricing)weight({ value, unit })- Optional filters like
carrier_code,contract_id,shipping_product_code,functionalities,dimensions,lead_time, etc.
Sample code
Copy the following code and paste it to a blank calculateShippingRates.js module and save it.
import { enrichItemDetails } from "./modules.js";
// ---------------------------
// Helpers
// ---------------------------
function b64Basic(user, pass) {
return "Basic " + btoa(`${user}:${pass}`);
}
function num(v, fallback) {
const n = Number(v);
return Number.isFinite(n) ? n : fallback;
}
function gramsToKg(grams) {
const g = Number(grams);
if (!Number.isFinite(g)) return 0;
return +(g / 1000).toFixed(3);
}
function sumShipmentWeightKg(DATA) {
// Prefer actual item grams (Shopify) if present
const items = Array.isArray(DATA?.items) ? DATA.items : [];
const totalGrams = items.reduce((acc, it) => acc + (Number(it?.grams) || 0) * (Number(it?.quantity) || 1), 0);
// Ensure a small non-zero weight so the API can respond predictably
return Math.max(gramsToKg(totalGrams), 0.001);
}
// Optional: build max dimensions from variant metafields (custom.length/width/height in CM)
// If metafields are missing, we omit "dimensions" completely.
function buildDimensionsCm(DATA) {
const items = Array.isArray(DATA?.items) ? DATA.items : [];
let maxL = 0, maxW = 0, maxH = 0;
for (const it of items) {
const mf = it?.metafields?.custom || {};
const l = num(mf.length, 0);
const w = num(mf.width, 0);
const h = num(mf.height, 0);
if (l > 0) maxL = Math.max(maxL, l);
if (w > 0) maxW = Math.max(maxW, w);
if (h > 0) maxH = Math.max(maxH, h);
}
if (maxL <= 0 || maxW <= 0 || maxH <= 0) return null;
return {
length: String(maxL.toFixed(2)),
width: String(maxW.toFixed(2)),
height: String(maxH.toFixed(2)),
unit: "cm",
};
}
// ---------------------------
// Main rate calculator
// ---------------------------
export async function calculateShippingRates(DATA, env) {
try {
// 1) (Optional) enrich dimensions from Shopify metafields
// If you don't store dimensions in metafields, you can remove this enrichment call.
DATA = await enrichItemDetails(DATA, [{ namespace: "custom", size: 10 }]);
const origin = DATA?.origin || {};
const destination = DATA?.destination || {};
// 2) Build Sendcloud request body (fetch-shipping-options)
const weightKg = sumShipmentWeightKg(DATA);
const body = {
from_country_code: (origin.country || "").toString(),
to_country_code: (destination.country || "").toString(),
from_postal_code: (origin.postal_code || "").toString(),
to_postal_code: (destination.postal_code || "").toString(),
weight: {
value: String(weightKg),
unit: "kg",
},
// Optional filters:
// carrier_code: "postnl",
// contract_id: 123,
// shipping_product_code: "postnl:small",
// functionalities: { signature: true },
// Optional insurance:
// total_insurance: 100.00,
};
const dims = buildDimensionsCm(DATA);
if (dims) body.dimensions = dims;
// 3) Call Sendcloud
const url = "https://panel.sendcloud.sc/api/v3/fetch-shipping-options";
if (!env?.SENDCLOUD_API_KEY || !env?.SENDCLOUD_API_SECRET) {
console.error("Missing Sendcloud credentials: SENDCLOUD_API_KEY / SENDCLOUD_API_SECRET");
return { rates: [] };
}
const resp = await fetch(url, {
method: "POST",
headers: {
Authorization: b64Basic(env.SENDCLOUD_API_KEY, env.SENDCLOUD_API_SECRET),
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const json = await resp.json().catch(() => ({}));
if (!resp.ok) {
console.error("Sendcloud error:", resp.status, json);
return { rates: [] };
}
// 4) Transform Sendcloud response -> Shopify rates
const options = Array.isArray(json?.data) ? json.data : [];
const rates = options
.map((opt) => {
// Quotes is typically an array; we take the first quote as the default price.
const q0 = Array.isArray(opt?.quotes) ? opt.quotes[0] : null;
// Prefer "total" price
const total = q0?.price?.total;
const value = total?.value;
const currency = total?.currency || DATA.currency || "EUR";
if (value == null) return null;
// Sendcloud lead_time is typically in hours; map to a simple description if present.
const leadTimeHrs = q0?.lead_time;
const description = Number.isFinite(Number(leadTimeHrs))
? `Lead time: ${leadTimeHrs}h`
: "";
return {
service_name: opt?.name || opt?.code || "Sendcloud",
service_code: opt?.code || "SENDCLOUD",
total_price: (Number(value) * 100).toFixed(0), // major -> minor units (string)
currency,
description,
// Delivery dates are not guaranteed by this endpoint; leave empty
min_delivery_date: "",
max_delivery_date: "",
};
})
.filter(Boolean);
return { rates };
} catch (err) {
console.error("Sendcloud Rate Error:", err?.message || err);
return { rates: [] };
}
}
Notes
- Postal codes: Sendcloud recommends providing
from_postal_codeandto_postal_codefor accurate pricing (including remote surcharges / zonal pricing where applicable). - Dimensions: If you provide
dimensions, Sendcloud can returnbilled_weightand more accurate quotes (depends on carrier/product). - Multiple parcels: If you need multi-parcel pricing (multi-collo), use the newer Sendcloud endpoint they recommend (see API docs).
Minimal request/response shape (quick view)
Request (example):
{
"from_country_code": "NL",
"to_country_code": "NL",
"weight": { "value": "2", "unit": "kg" },
"carrier_code": "postnl",
"functionalities": { "signature": true }
}
Response (example):
{
"data": [
{
"code": "postnl:small/home_address_only,signature",
"name": "PostNL Klein Pakket ...",
"carrier": { "code": "postnl", "name": "PostNL" },
"quotes": [
{
"price": { "total": { "value": "0", "currency": "EUR" } },
"lead_time": 24
}
]
}
]
}
Sendcloud API documentation references
This JsRates example is implemented using the Sendcloud Shipping Options – Fetch shipping options endpoint.
For full details and the authoritative schema, always refer to the official Sendcloud API documentation.
Endpoint used in this implementation
- Fetch shipping options (single-shipment quotes)
https://api.sendcloud.dev/docs/sendcloud-public-api/shipping-options/operations/create-a-fetch-shipping-option
This endpoint is suitable when:
- You want live quotes based on total shipment weight
- You are not explicitly modelling multiple parcels
- You prefer a simpler request payload with fewer required fields
Alternative endpoint: full shipment / multi-parcel pricing
Sendcloud also provides a more expressive endpoint for full shipment modelling:
- Create shipping option (full shipment model)
https://api.sendcloud.dev/docs/sendcloud-public-api/shipping-options/operations/create-a-shipping-option
This endpoint is recommended when:
- You calculate or control multiple parcels (cartons)
- You need per-parcel dimensions and weights
- You want the most accurate pricing and access to advanced features (such as service-point delivery)
Important:
The two endpoints are related but not interchangeable.
/shipping-optionssupports a full shipment model with parcels, while/fetch-shipping-optionsis optimized for simpler quote lookups.
